// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! This module manages the state of a Linera chain, including cross-chain communication.

pub mod block;
mod certificate;

pub mod types {
    pub use super::{block::*, certificate::*};
}

mod block_tracker;
mod chain;
pub mod data_types;
mod inbox;
pub mod manager;
mod outbox;
mod pending_blobs;
#[cfg(with_testing)]
pub mod test;

pub use chain::ChainStateView;
use data_types::{MessageBundle, PostedMessage};
use linera_base::{
    bcs,
    crypto::CryptoError,
    data_types::{ArithmeticError, BlockHeight, Round, Timestamp},
    identifiers::{ApplicationId, ChainId},
};
use linera_execution::ExecutionError;
use linera_views::ViewError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ChainError {
    #[error("Cryptographic error: {0}")]
    CryptoError(#[from] CryptoError),
    #[error(transparent)]
    ArithmeticError(#[from] ArithmeticError),
    #[error(transparent)]
    ViewError(#[from] ViewError),
    #[error("Execution error: {0} during {1:?}")]
    ExecutionError(Box<ExecutionError>, ChainExecutionContext),

    #[error("The chain being queried is not active {0}")]
    InactiveChain(ChainId),
    #[error(
        "Cannot vote for block proposal of chain {chain_id} because a message \
         from chain {origin} at height {height} has not been received yet"
    )]
    MissingCrossChainUpdate {
        chain_id: ChainId,
        origin: ChainId,
        height: BlockHeight,
    },
    #[error(
        "Message in block proposed to {chain_id} does not match the previously received messages from \
        origin {origin:?}: was {bundle:?} instead of {previous_bundle:?}"
    )]
    UnexpectedMessage {
        chain_id: ChainId,
        origin: ChainId,
        bundle: Box<MessageBundle>,
        previous_bundle: Box<MessageBundle>,
    },
    #[error(
        "Message in block proposed to {chain_id} is out of order compared to previous messages \
         from origin {origin:?}: {bundle:?}. Block and height should be at least: \
         {next_height}, {next_index}"
    )]
    IncorrectMessageOrder {
        chain_id: ChainId,
        origin: ChainId,
        bundle: Box<MessageBundle>,
        next_height: BlockHeight,
        next_index: u32,
    },
    #[error(
        "Block proposed to {chain_id} is attempting to reject protected message \
        {posted_message:?}"
    )]
    CannotRejectMessage {
        chain_id: ChainId,
        origin: ChainId,
        posted_message: Box<PostedMessage>,
    },
    #[error(
        "Block proposed to {chain_id} is attempting to skip a message bundle \
         that cannot be skipped: {bundle:?}"
    )]
    CannotSkipMessage {
        chain_id: ChainId,
        origin: ChainId,
        bundle: Box<MessageBundle>,
    },
    #[error(
        "Incoming message bundle in block proposed to {chain_id} has timestamp \
        {bundle_timestamp:}, which is later than the block timestamp {block_timestamp:}."
    )]
    IncorrectBundleTimestamp {
        chain_id: ChainId,
        bundle_timestamp: Timestamp,
        block_timestamp: Timestamp,
    },
    #[error("The signature was not created by a valid entity")]
    InvalidSigner,
    #[error(
        "Chain is expecting a next block at height {expected_block_height} but the given block \
        is at height {found_block_height} instead"
    )]
    UnexpectedBlockHeight {
        expected_block_height: BlockHeight,
        found_block_height: BlockHeight,
    },
    #[error("The previous block hash of a new block should match the last block of the chain")]
    UnexpectedPreviousBlockHash,
    #[error("Sequence numbers above the maximal value are not usable for blocks")]
    BlockHeightOverflow,
    #[error(
        "Block timestamp {new} must not be earlier than the parent block's timestamp {parent}"
    )]
    InvalidBlockTimestamp { parent: Timestamp, new: Timestamp },
    #[error("Round number should be at least {0:?}")]
    InsufficientRound(Round),
    #[error("Round number should be greater than {0:?}")]
    InsufficientRoundStrict(Round),
    #[error("Round number should be {0:?}")]
    WrongRound(Round),
    #[error("Already voted to confirm a different block for height {0:?} at round number {1:?}")]
    HasIncompatibleConfirmedVote(BlockHeight, Round),
    #[error("Proposal for height {0:?} is not newer than locking block in round {1:?}")]
    MustBeNewerThanLockingBlock(BlockHeight, Round),
    #[error("Cannot confirm a block before its predecessors: {current_block_height:?}")]
    MissingEarlierBlocks { current_block_height: BlockHeight },
    #[error("Signatures in a certificate must be from different validators")]
    CertificateValidatorReuse,
    #[error("Signatures in a certificate must form a quorum")]
    CertificateRequiresQuorum,
    #[error("Internal error {0}")]
    InternalError(String),
    #[error("Block proposal has size {0} which is too large")]
    BlockProposalTooLarge(usize),
    #[error(transparent)]
    BcsError(#[from] bcs::Error),
    #[error("Closed chains cannot have operations, accepted messages or empty blocks")]
    ClosedChain,
    #[error("Empty blocks are not allowed")]
    EmptyBlock,
    #[error("All operations on this chain must be from one of the following applications: {0:?}")]
    AuthorizedApplications(Vec<ApplicationId>),
    #[error("Missing operations or messages from mandatory applications: {0:?}")]
    MissingMandatoryApplications(Vec<ApplicationId>),
    #[error("Executed block contains fewer oracle responses than requests")]
    MissingOracleResponseList,
    #[error("Not signing timeout certificate; current round does not time out")]
    RoundDoesNotTimeOut,
    #[error("Not signing timeout certificate; current round times out at time {0}")]
    NotTimedOutYet(Timestamp),
}

impl ChainError {
    /// Returns whether this error is caused by an issue in the local node.
    ///
    /// Returns `false` whenever the error could be caused by a bad message from a peer.
    pub fn is_local(&self) -> bool {
        match self {
            ChainError::CryptoError(_)
            | ChainError::ArithmeticError(_)
            | ChainError::ViewError(ViewError::NotFound(_))
            | ChainError::InactiveChain(_)
            | ChainError::IncorrectMessageOrder { .. }
            | ChainError::CannotRejectMessage { .. }
            | ChainError::CannotSkipMessage { .. }
            | ChainError::IncorrectBundleTimestamp { .. }
            | ChainError::InvalidSigner
            | ChainError::UnexpectedBlockHeight { .. }
            | ChainError::UnexpectedPreviousBlockHash
            | ChainError::BlockHeightOverflow
            | ChainError::InvalidBlockTimestamp { .. }
            | ChainError::InsufficientRound(_)
            | ChainError::InsufficientRoundStrict(_)
            | ChainError::WrongRound(_)
            | ChainError::HasIncompatibleConfirmedVote(..)
            | ChainError::MustBeNewerThanLockingBlock(..)
            | ChainError::MissingEarlierBlocks { .. }
            | ChainError::CertificateValidatorReuse
            | ChainError::CertificateRequiresQuorum
            | ChainError::BlockProposalTooLarge(_)
            | ChainError::ClosedChain
            | ChainError::EmptyBlock
            | ChainError::AuthorizedApplications(_)
            | ChainError::MissingMandatoryApplications(_)
            | ChainError::MissingOracleResponseList
            | ChainError::RoundDoesNotTimeOut
            | ChainError::NotTimedOutYet(_)
            | ChainError::MissingCrossChainUpdate { .. } => false,
            ChainError::ViewError(_)
            | ChainError::UnexpectedMessage { .. }
            | ChainError::InternalError(_)
            | ChainError::BcsError(_) => true,
            ChainError::ExecutionError(execution_error, _) => execution_error.is_local(),
        }
    }
}

#[derive(Copy, Clone, Debug)]
#[cfg_attr(with_testing, derive(Eq, PartialEq))]
pub enum ChainExecutionContext {
    Query,
    DescribeApplication,
    IncomingBundle(u32),
    Operation(u32),
    Block,
}

pub trait ExecutionResultExt<T> {
    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError>;
}

impl<T, E> ExecutionResultExt<T> for Result<T, E>
where
    E: Into<ExecutionError>,
{
    fn with_execution_context(self, context: ChainExecutionContext) -> Result<T, ChainError> {
        self.map_err(|error| ChainError::ExecutionError(Box::new(error.into()), context))
    }
}
